package com.zee.common.utils;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;

import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * sign签名工具类
 *
 * @author LinZee
 * @email linzee666@163.com
 */
public class SignUtil {

    //有效时间，单位秒
    public static final Integer EXPIRE_TIME = 60 * 10;

    /**
     * google guava包下的缓存,Local Cache  EXPIRE秒过期
     */
    public static Cache<String, String> localCache =
            CacheBuilder.newBuilder().maximumSize(10000).expireAfterAccess(EXPIRE_TIME, TimeUnit.SECONDS).build();


    /**
     * 验证签名是否正确，只验证时间戳(timestamp)是否过期
     *
     * @param map     参数，map键值至少包含key，timestamp
     * @param secret  密钥
     * @param signStr 待验证签名
     * @return 是否正确
     */
    public static boolean verifySimple(Map<String, Object> map, String secret, String signStr) {
        //校验参数
        if (map.get("timestamp") == null || "".equals(map.get("timestamp"))) {
            throw new RuntimeException("没有携带参数timestamp");
        }
        //字符串转大写
        signStr = signStr.toUpperCase();
        String timestamp = (String) map.get("timestamp");

        //判断时间戳是否在有效的规定时间内（注意双方的服务器要在同一时区，并且双方服务器时间差不能大于EXPIRE）
        if (System.currentTimeMillis() - Long.parseLong(timestamp) * 1000 > EXPIRE_TIME * 1000) {
            throw new RuntimeException("sign签名已过期");
        }

        //调用生成签名方法，然后验签
        if (sign(map, secret).equals(signStr)) {
            return true;
        }
        return false;
    }

    /**
     * 验证签名是否正确，需要验证时间戳(timestamp)和随机字符串(nonce),所以此方法需要开启缓存的前提下使用
     *
     * @param map     参数，map键值至少包含key，timestamp， nonce
     * @param secret  密钥
     * @param signStr 待验证sign字符串
     * @return 是否正确
     */
    public static boolean verify(Map<String, Object> map, String secret, String signStr) {
        //校验参数
        if (map.get("timestamp") == null || "".equals(map.get("timestamp"))) {
            throw new RuntimeException("没有携带参数timestamp");
        }
        if (map.get("nonce") == null || "".equals(map.get("nonce"))) {
            throw new RuntimeException("没有携带参数nonce");
        }
        //字符串转大写
        signStr = signStr.toUpperCase();
        String key = (String) map.get("key");
        String nonce = (String) map.get("nonce");
        String timestamp = (String) map.get("timestamp");

        //判断时间戳是否在有效的规定时间内（双方的服务器要在同一时区，并且双方服务器时间差不能大于EXPIRE_TIME），并且随机字符串nonce只调用了一次
        if (System.currentTimeMillis() - Long.parseLong(timestamp) * 1000 > EXPIRE_TIME * 1000) {
            throw new RuntimeException("sign签名已过期");
        }

        //存过缓存则nonce用过了
        if (localCache.getIfPresent(key + ":" + nonce) != null) {
            throw new RuntimeException("sign签名已使用");
        }
        //调用生成签名方法，然后验签正确要缓存随机字符串nonce
        if (sign(map, secret).equals(signStr)) {
            localCache.put(key + ":" + nonce, "1");
            return true;
        }
        return false;
    }

    /**
     * 生成签名
     *
     * @param map    参数
     * @param secret 密钥
     * @return sign 签名字符串
     */
    public static String sign(Map<String, Object> map, String secret) {
        if (map.get("key") == null || "".equals(map.get("key"))) {
            throw new RuntimeException("没有携带参数key");
        }
        //参数排序
        String sortStr = getAsciiSort(map);
        StringBuilder sb = new StringBuilder();
        sb.append(sortStr).append("&secret=").append(secret);
        System.out.println(sb);
        //md5加密
        byte[] secretBytes = null;
        try {
            secretBytes = MessageDigest.getInstance("md5").digest(
                    sb.toString().getBytes(StandardCharsets.UTF_8));
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("没有md5算法");
        }

        //转成大写字符串
        return new BigInteger(1, secretBytes).toString(16).toUpperCase();
    }

    /**
     * 根据键值按ASCII码从小到大排序并用&连接，值为空的不拼接
     *
     * @param map 参数
     * @return 参数字符串
     */
    private static String getAsciiSort(Map<String, Object> map) {
        //移除值为空的和键名为sign的参数
        map.entrySet().removeIf(
                entry -> Objects.isNull(entry.getValue())
                        || "".equals(entry.getValue())
                        || "sign".equals(entry.getKey()));

        List<Map.Entry<String, Object>> infoIds = new ArrayList<>(map.entrySet());
        //对所有传入参数按照字段名的ASCII码从小到大排序（字典序）
        infoIds.sort((o1, o2) -> o1.getKey().compareToIgnoreCase(o2.getKey()));
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<String, Object> infoId : infoIds) {
            sb.append(infoId.getKey());
            sb.append("=");
            sb.append(infoId.getValue());
            sb.append("&");
        }
        return sb.substring(0, sb.length() - 1);
    }

    public static void main(String[] args) {
        //Key值
        String key = "d05ea5d93a258f4f";
        //密钥
        String secret = "247ec02edce69192006250b4c09f6a2d";
        Date date = new Date();
        long time = date.getTime();

        //随机字符串
        String nonce = time + "";
        //时间戳，单位秒
        String timestamp = String.valueOf(time / 1000);

        HashMap<String, Object> map = new HashMap<>();
        map.put("key", key);
        map.put("nonce", nonce);
        map.put("timestamp", timestamp);
        map.put("name", "linzee");
        System.out.println(map);
        String sign = sign(map, secret);
        System.out.println(sign);
        boolean verify = verify(map, secret, sign);
        boolean verify1 = verify(map, secret, sign);
    }
}
